Nom : Aka Yei Noellie , Lignier Léa
M2 MSID

Perceptron Multi-couches pour la classification d'images MNIST¶

Introduction¶

A l'ère de l'intelligence artficielle,l'automatisation des tâches à faible valeur ajoutée est devenue une problématique cruciale pour les entreprises. Ces dernières trouvent une solution dans le Deep Learning, en passant par ses méthodes les plus récentes au plus anciennes.
Un exemple populaire d'utilisation pratique est dans le domaine de la classification d'images. Grâce aux données de la base MNIST, nous essayerons de vous montrer comment le modèle du Perceptron Multi-couches peut être utilisé pour reconnaître des chiffres manuscrits.

Sommaire :¶

  • 1.Implémentation avec Python
    • 1.1. Chargement des librairies
    • 1.2. Définition du modèle
    • 1.3. Visualisation des fonctions de perte et accuracy
    • 1.4. Visualisation des images
  • 2.Préparation des données
    • 2.1. Chargement des données et découpage des données
    • 2.2. Normalisation des données
    • 2.3. Seuillage d'images
  • 3.Élaboration de l’expérimentation et discussion des résultats
    • 3.1.Expérimentation avec des données normalisées
    • 3.1.Expérimentation avec des données binarisées
  • Conclusion

1. Implémentation avec python ¶

Dans cette partie, nous expliquerons et implémenterons toutes les fonctions utiles à la réalisation de notre classification.

1.1. Chargement des librairies ¶

In [4]:
#librairie traitement des données
import numpy as np
import pandas as pd

# Librairie graphiques
import matplotlib
import matplotlib.pyplot as plt
import plotly
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px 
import seaborn as sns
import plotly.io as pio

# Librairies Stat
import tensorflow as tf
from tensorflow import keras
import sklearn
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import confusion_matrix
import statistics

#Librairies Système
from datetime import datetime
import os

print("numpy V. ", np.__version__)
print("pandas V. ", pd.__version__)
print("tensorflow V. ", tf.__version__)
print("Matplotlib", matplotlib.__version__)
print("Plotly V. ", plotly.__version__)
print("sklearn V. ", sklearn.__version__)
print("seaborn V. ", sns.__version__)
numpy V.  1.23.2
pandas V.  1.4.3
tensorflow V.  2.10.0
Matplotlib 3.5.3
Plotly V.  5.10.0
sklearn V.  0.24.2
seaborn V.  0.12.1

1.2. Modèle ¶

Model_PMC est la fonction qui nous permettra de créer notre perceptron multi-couches. Dans cette fonction, on définit à la suite les couches cachées et la couche de sortie. Les hyper-paramètres sur lesquelles nous jouerons pour l'ensemble de ces couches sont : le nombre de neurones (units) et les fonctions d'activation (activation). Nous compilerons ensuite le modèle.

  • n_cc : Nombre de couches cachées
  • p_cc : paramètres des couches cachées (nombre de neurones et fonction d'activation)
  • p_comp : paramètres du compilateur (optimizer, loss et metrics)
  • p_sort : paramètres de la couche de sortie (nombre de classes, fonction d'activation)
In [5]:
def Model_PMC (n_cc,p_cc,p_comp,p_sort) :
    model = keras.Sequential()
    
    # Définition des couches cachées
    for i in range(n_cc) :
        model.add(keras.layers.Dense(p_cc['n_neur'][i],activation=p_cc['act'][i]))
    
    # Définition de la couche de sortie 
    model.add(keras.layers.Dense(p_sort['n_class'],activation=p_sort['act']))
    
    # Compilation du modèle
    model.compile(optimizer=p_comp['opt'],loss=p_comp['loss'],metrics=p_comp['metrics'])
    
    return model  

1.3 Visualisation des fonctions de perte et accuracy¶

Nous allons créer une fonction capable de tracer l'accuracy et la fonction de perte pour les données d'apprentissage et de validation.

  • loss_train : Les valeurs de perte des données d'entraînement
  • acc_train : Les valeurs d'accuracy des données d'entraînement
  • loss_val : Les valeurs de perte des données de validation
  • acc_train : Les valeurs d'accuracy des données de validation

*Nb* : Si loss_val = None, les courbes des données de validation ne seront pas tracées.

In [6]:
def plot_qualit (loss_train,acc_train,loss_val=None,acc_val=None) :
    if loss_val is not None : 
        fig = make_subplots(rows=2, cols=1, subplot_titles=("Loss", "Accuracy"))
                        
        fig.add_trace(
            go.Scatter(y=loss_train,name='loss train'),
            row=1, col=1,secondary_y=False
        )

        fig.add_trace(
            go.Scatter (y=loss_val,name='loss val'),
            row=1, col=1, secondary_y=False
        )

        fig.add_trace(
            go.Scatter(y=acc_train,name='accuracy train'),
            row=2, col=1, secondary_y=False,
        )
        fig.add_trace(
            go.Scatter(y=acc_val,name='accuracy val'),
            row=2, col=1, secondary_y=False,
        )
        fig.update_layout(height=800, width=800, title_text="Fonction de perte et accuracy")
        pio.renderers.default='notebook'
        fig.show("notebook")
    else : 
        fig = make_subplots(rows=2, cols=1,subplot_titles=("Loss", "Accuracy"))
        fig.add_trace(
            go.Scatter(y=loss_train,name='loss train'),
            row=1, col=1
        )

        fig.add_trace(
            go.Scatter(y=acc_train,name='accuracy loss'),
            row=2, col=1
        )
        fig.update_layout(height=800, width=800, title_text="Fonction de perte et accuracy")
        pio.renderers.default='notebook'
        fig.show("notebook")

1.4 Visualition des images ¶

La fonction plot_img nous permet de visualiser les images d'un dataset.

  • data = matrice des coordonnées des images
  • label = vecteur des numéros des images
  • prediction = True si on met le vecteur des mauvaises prédictions

*Nb:* Si la valeur de prédiction est true, les images seront affichées en rouge et leurs mauvaises prédictions en dessous.

In [7]:
def plot_img (data,label,prediction=True) :
    diviseurs = [i for i in range(1,len(data)+1) if len(data) % i == 0]
    data = data.values.reshape(data.shape[0], 28, 28,1)
    if len(diviseurs)>=2 :
        n_cols =  int(diviseurs[len(diviseurs) // 2])
    else :
        n_cols = len(data)
    n_rows = int(len(data)/n_cols)
    fig, ax = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(20, 4))
    ax=ax.flatten()
    if prediction is False :
        for i in range(len(data)):
            ax[i].imshow(data[i], cmap='gray')
            ax[i].set_xlabel(label[i])
    else :
        for i in range(len(data)):
            ax[i].imshow(data[i], cmap='Reds')
            ax[i].set_xlabel(label[i])
    plt.tight_layout()
    plt.show()

2. Préparation des données ¶

2.1. Chargement des données et découpage des données ¶

Nous avons 60000 images dans le fichier "mnist_train-e.csv" et 10000 dans le fichier "mnist_test-e.csv". Observons la répartition des classes dans notre fichier train :

In [8]:
data = pd.read_csv("mnist_train-e.csv")
y = data.pop('1')
sns.histplot(data=y);

Nous observons que certaines classes ont plus d'effectifs que d'autres. Dans un soucis d'équilibre, nous allons découper les données du fichier "mnist_train-e.csv" de la manière suivante :

  • Les données train contiendront 4800 images de chaque classe soit 80% de la taille du fichier "mnist_train-e.csv".
  • Les 20% restantes seront consacrées aux données de validation.
  • *new_idx* contiendra les indexes des 4800 premières images de chaque classe et *rest_idx* le reste des indexes.
  • Nous séparerons la colonne classe des autres colonnes de chaque set de données.
In [9]:
new_idx = data.index[y==0][:4800]
rest_idx = data.index[y==0][4800:]
for i in np.arange(1,10) :
    new_idx = new_idx.append(data.index[y==i][:4800])
    rest_idx = rest_idx.append(data.index[y==i][4800:])
  • train_data,val_data et test_data contiennent respectivement les données d'apprentissage, de validation et de test.
  • y_train,y_val et y_test quant à elles contiennent respectivement les classes de chaque image pour les données d'apprentissage, de validation et de test.
In [10]:
train_data= data.iloc[new_idx]
y_train = y[new_idx]
val_data= data.iloc[rest_idx]
y_val = y[rest_idx]
test_data = pd.read_csv("mnist_test-e.csv")
y_test =test_data.pop('1')
In [11]:
print("Les cinq premières lignes des données d'apprentissage:")
train_data.head(5)
Les cinq premières lignes des données d'apprentissage:
Out[11]:
2 3 4 5 6 7 8 9 10 11 ... 776 777 778 779 780 781 782 783 784 785
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
21 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
34 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
37 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
51 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 784 columns

In [12]:
print("Visualisation des 12 premières images de test:")
plot_img(test_data[0:12],y_test[0:12],prediction=False)
Visualisation des 12 premières images de test:

2.2. Normalisation des données ¶

Le deuxième traitement que nous allons appliquer à nos images est la normalisation des données grâce à l'étendue. Normaliser les données permet de ne pas faire exploser le calcul des fonctions et d'avoir une échelle restreinte entre 0 et 1.
$$ x_{new}=\frac{x_{old}-min}{max-min}$$
La fonction qui permet de faire cette opération avec Python est *MinMaxScaler* et provient du package sklearn. Nos nouvelles données s'appelleront train_data_norm, val_data_norm et test_data_norm.

In [13]:
scaler = MinMaxScaler()
train_data_norm = pd.DataFrame(scaler.fit_transform(train_data))
val_data_norm = pd.DataFrame(scaler.fit_transform(val_data))
test_data_norm = pd.DataFrame(scaler.fit_transform(test_data))

2.3. Seuillage des images ¶

Le seuillage d'image est une technique de binarisation d'image1, consistant à remplacer les niveaux de gris d'une image par un ensemble de pixels prenant la valeur 255 (blanc) ou 0 (noir) selon que sa valeur initiale est inférieure ou supérieure à une valeur définie comme seuil. Nous avons créé une fonction binarise qui prend en entrée :

  • tab : un tableau de données
  • seuil : un seuil

Elle retourne le tableau de données avec de nouvelles coordonnées (1 ou 0)

In [14]:
def binarise(tab, seuil):
    for i in range(0,tab.shape[0],1) :
        for j in range(0,tab.shape[1],1) :
            if tab[i][j] >= seuil :
                tab[i][j] = 0 #noir
            else :
                tab[i][j] = 1 #blanc
    return(tab)

*NB:* Le seuillage et la normalisation ont été appliqués directement sur le jeu de données équilibré et de façon indépendante.

3. Élaboration de l’expérimentation et discussion des résultats ¶

Pour chaque traitement effectué dans les données précédentes, nous allons faire varier les paramètres, afficher l'accuracy et la fonction de perte. Nous allons entraîner nos données normalisées puis celles binarisées en faisant varier les hyper-paramètres.

3.1. Expérimentation avec des données normalisées¶

Dans ce premier modèle, nous avons décidé d'utiliser deux couches et de faire une combinaison entre plusieurs valeurs de nombre de neurones allant de 50 à 550 pour décider du nombre de neurones à choisir dans chaque couche. En résultat, nous avons un tableau nommé *test1* qui donne les dernières valeurs de l'accuracy et de la fonction de perte pour les données d'apprentissage et de validation.
Le temps d'éxécution étant long, nous avons exporté nos résultats en fichier csv pour ne pas avoir à relancer le code ci-dessous.

  • La fonction d'activation utilisée pour les couches cachées est la fonction relu et softmax pour la couche de sortie
  • L'optimiseur est Adam et nous utilisons la fonction de perte CategoricalCrossEntropy avec pour métrique l'accuracy
In [108]:
n_cc = 2
cc1 = range(50,600,100) # couche cachée 1
cc2 = range(50,600,100) # couche cachée 2
test1= pd.DataFrame({'cc1' : [],'cc2':[],'loss_train':[],'loss_val':[],'acc_train':[],'acc_val':[]})
for i in cc1 :
    for j in cc2 :
        if i>=j :
            p_cc = {'n_neur':[i,j],'act':['relu','relu']}
            p_sort = {'n_class':10,'act':'softmax'}
            p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
            model = Model_PMC (n_cc,p_cc,p_comp,p_sort) 
            model_entrain_norm = model.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=10,batch_size=128,verbose=0)
            loss_train_norm = model_entrain_norm.history['loss'][-1]
            acc_train_norm = model_entrain_norm.history['accuracy'][-1]
            loss_val_norm = model_entrain_norm.history['val_loss'][-1]
            acc_val_norm = model_entrain_norm.history['val_accuracy'][-1]
            new_row = pd.Series({'cc1':i,'cc2':j,'loss_train':loss_train_norm,'loss_val':loss_val_norm,'acc_train':acc_train_norm,'acc_val': acc_val_norm })
            test1 = pd.concat([test1, new_row.to_frame().T], ignore_index=True)
                    
In [114]:
print("Voici le resultat rangés en fonction de l'accuracy de test décroissant:")
test1.sort_values(by='acc_train',ascending=False)
Voici le resultat rangés en fonction de l'accuracy de test décroissant:
Out[114]:
cc1 cc2 loss_train loss_val acc_train acc_val
13 450.0 350.0 0.007970 0.094575 0.997563 0.978917
10 450.0 50.0 0.010948 0.090550 0.996792 0.977083
11 450.0 150.0 0.010210 0.093104 0.996708 0.978583
3 250.0 50.0 0.012410 0.087733 0.996604 0.977917
6 350.0 50.0 0.012065 0.089200 0.996500 0.978917
16 550.0 150.0 0.012107 0.101927 0.996208 0.975333
18 550.0 350.0 0.011005 0.106295 0.996104 0.977750
15 550.0 50.0 0.012920 0.095132 0.996042 0.978333
12 450.0 250.0 0.012211 0.098431 0.995833 0.977667
14 450.0 450.0 0.013545 0.123933 0.995667 0.976417
7 350.0 150.0 0.012990 0.094927 0.995646 0.977333
9 350.0 350.0 0.013087 0.095526 0.995521 0.977417
8 350.0 250.0 0.013683 0.118810 0.995438 0.972417
17 550.0 250.0 0.013904 0.083680 0.995292 0.979000
20 550.0 550.0 0.013932 0.115236 0.995292 0.976167
2 150.0 150.0 0.015847 0.090988 0.995250 0.977000
4 250.0 150.0 0.015734 0.087259 0.995083 0.977833
19 550.0 450.0 0.015997 0.094665 0.994917 0.978500
5 250.0 250.0 0.016731 0.089275 0.994563 0.978583
1 150.0 50.0 0.020064 0.087363 0.994479 0.976917
0 50.0 50.0 0.061505 0.104928 0.981104 0.970250
In [ ]:
test1.to_csv("result_test1.csv")

Nous allons retenir le résultat de la 13e ligne car nous obtenons pour les données d'apprentissage la plus petite valeur de la fonction perte 0.007970 et la meilleure accuracy 0.997563. Nous aurons ainsi *450* neurones sur la première couche cachée et *350* sur la deuxième.

Modèle 1 : Deux couches avec pour nombre de neurones le couple (450,350)¶

Le nombre d'epochs est fixé à 10 et le batch à 128.

In [16]:
#hyperparamètres
n_cc = 2 # nombre de couches cachées
p_cc = {'n_neur':[450,350],'act':['relu','relu']} #nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}

#Application :
model = Model_PMC (n_cc,p_cc,p_comp,p_sort) 
model_entrain_norm = model.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=10,batch_size=128,verbose=0)
loss_train_norm = model_entrain_norm.history['loss']
acc_train_norm = model_entrain_norm.history['accuracy']
loss_val_norm = model_entrain_norm.history['val_loss']
acc_val_norm = model_entrain_norm.history['val_accuracy']

# Fonction de perte et accuracy pour les données test
loss_test_norm, acc_test_norm= model.evaluate(test_data_norm,y_test)
print("Erreur d'appentissage:",round(loss_train_norm[-1],3))
print("Accuracy d'appentissage:",round(acc_train_norm[-1],3))
print("Erreur de test:",round(loss_test_norm,3))
print("Accuracy de test:",round(acc_test_norm,3))

# Representation
plot_qualit(loss_train_norm,acc_train_norm,loss_val=loss_val_norm,acc_val=acc_val_norm)
313/313 [==============================] - 2s 6ms/step - loss: 0.0715 - accuracy: 0.9827
Erreur d'appentissage: 0.01
Accuracy d'appentissage: 0.997
Erreur de test: 0.071
Accuracy de test: 0.983
In [17]:
#sauvegarde
model.save=("model_norm.h5")

Avec une faible valeur d'epochs, nous obtenons une valeur d'accuracy à 0.997. Augmentons le nombre d'epochs afin de voir si le modèle apprend mieux (toujours avec le couple (450,350))

Modèle 2 : Augmentation du nombre d'epochs du modèle 1¶

In [23]:
model_norm1 = Model_PMC(n_cc,p_cc,p_comp,p_sort) 
n_cc = 2 # nombre de couches cachées
p_cc = {'n_neur':[450,350],'act':['relu','relu']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
model_entrain_norm1 = model_norm1.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=20,batch_size=128,verbose=0)
loss_train_norm1 = model_entrain_norm1.history['loss']
acc_train_norm1 = model_entrain_norm1.history['accuracy']
loss_val_norm1 = model_entrain_norm1.history['val_loss']
acc_val_norm1 = model_entrain_norm1.history['val_accuracy']


#Fonction de perte et accuracy pour les données test

loss_test_norm1, acc_test_norm1 = model_norm1.evaluate(test_data_norm,y_test)
print("Erreur d'appentissage:",round(loss_train_norm1[-1],3))
print("Accuracy d'appentissage:",round(acc_train_norm1[-1],3))
print("Erreur de test:",round(loss_test_norm1,3))
print("Accuracy de test:",round(acc_test_norm1,3))

plot_qualit(loss_train_norm1,acc_train_norm1,loss_val=loss_val_norm1,acc_val=acc_val_norm1)
313/313 [==============================] - 2s 5ms/step - loss: 0.1005 - accuracy: 0.9821
Erreur d'appentissage: 0.005
Accuracy d'appentissage: 0.998
Erreur de test: 0.1
Accuracy de test: 0.982

L'augmentation du nombre d'epochs permet d'améliorer faiblement notre modèle. En effet, l'accuracy des données d'apprentissage augmente *(+0.001)* et leur fonction de perte passe de 0.01 à 0.005. Quant aux données de test, elles ont dû mal à s'adapter à ce modèle. En effet, leur accuracy baisse de 0.001 avec une erreur d'apprentissage qui augmente de 0.004. Dans la suite, nous utiliserons le modèle avec 10 epochs.

In [24]:
model_norm1.save=("model_norm1.h5")
Visualisation de mauvaises prédictions¶

Nous utiliserons le modèle avec 10 epochs puisqu'il a une meilleure valeur d'accuracy de test.

In [25]:
# Prédictions test
predict_proba_norm = model.predict(test_data_norm);
predictions_norm= np.argmax(predict_proba_norm, axis=1);
print("Représentation de quelques mauvaises prédictions:")
plot_img(test_data_norm[y_test!=predictions_norm][0:5],label=predictions_norm[y_test!=predictions_norm][0:5],prediction=True)
313/313 [==============================] - 2s 5ms/step
Représentation de quelques mauvaises prédictions:
Matrice de confusion¶
In [26]:
mc_norm = pd.DataFrame(confusion_matrix(y_test,predictions_norm),columns=range(0,10))
mc_norm['Total mauvaises predict'] = [np.sum(mc_norm.iloc[i])-mc_norm.loc[i,i] for i in range(0,10)]
mc_norm
Out[26]:
0 1 2 3 4 5 6 7 8 9 Total mauvaises predict
0 971 1 1 0 0 1 2 0 2 2 9
1 0 1123 2 2 0 1 3 1 3 0 12
2 2 0 1016 2 3 0 1 5 3 0 16
3 0 0 3 991 0 9 0 2 4 1 19
4 1 0 2 0 956 1 5 6 0 11 26
5 2 0 0 4 0 879 4 0 2 1 13
6 4 2 0 1 2 2 947 0 0 0 11
7 1 0 6 2 0 0 0 1010 2 7 18
8 0 1 6 5 2 3 2 4 948 3 26
9 3 2 0 2 6 3 2 4 1 986 23

Les plus mauvaises prédictions viennent des images de la classe 4 et de la classe 8. Intéressons nous à la classe 4. 11 images de cette classe sont prédites comme appartenant à la classe 9. Affichons ces images mal prédites de cette classe. Pour rappel, les classes prédites par notre modèle sont affichées en dessous de chaque image.

In [27]:
plot_img(test_data_norm[(y_test==4) &(predictions_norm==9)][0:11],label=predictions_norm[(y_test==4) &(predictions_norm==9)][0:11],prediction=True)

Répresentons également quelques images de la classe 4 qui sont bien prédites :

In [28]:
plot_img(test_data_norm[(y_test==4) &(predictions_norm==4)][0:5],label=predictions_norm[(y_test==4) &(predictions_norm==4)][0:5],prediction=False)

Répresentons également quelques images de la classe 9 :

In [29]:
plot_img(test_data_norm[(y_test==9)][0:3],label=predictions_norm[(y_test==9)][0:3],prediction=False)

On peut observer que les images de la classe 4 qui sont prédites en 9 sont celles qui sont fermées ( voir ci-dessous à quoi correspond un 4 fermé) tandis que les 4 bien prédits sont ceux ouverts. Lorsque le "4 est ouvert", il est fort ressemblant au chiffre 9. image-2.png

La classe ayant le meilleur taux de prédiction est la classe 0. Affichons les images mal prédites de cette classe :

In [30]:
plot_img(test_data_norm[(y_test==0) &(predictions_norm!=0)][0:9],label=predictions_norm[(y_test==0) &(predictions_norm!=0)][0:9],prediction=True)

On peut observer que ces images sont soient manquantes de quelques parties (exemple 2e image de la deuxième ligne) soient ont des traits en plus qui sont rajoutés (3e image de la troisième ligne). Ces erreurs sont principalement dûes à la mauvaise écriture manuscrite.

3.2 Expérimentation avec des données binarisées¶

Nous allons représenter la distribution de nos valeurs de pixels dans nos données d'entraînement en supprimant les valeurs aberrantes (pixel <10 et pixel>199).

In [32]:
count_values = np.bincount(np.array(train_data).flatten())
plt.plot(range(10,199), count_values[10:199]);
plt.xlabel("Valeur de pixels")
plt.ylabel("Nombre de pixels")
plt.title("Distribution des pixels sur les données d'entraînement");

Nous allons choisir un seuil avant le premier pic (environ 68) pour essayer d'équilibrer la binarisation.

Modèle 3 : Application de la même configuration que le modèle n°2¶

Nous allons ici réaliser la même configuration que le modèle n°2 mais en l'appliquant sur les données binarisées avec un seuil égal à la moyenne des valeurs de pixel, qui vient avant le premier pic.
Calculons la moyenne de nos données :

In [33]:
avg = round(np.mean(np.array(train_data).flatten()),0)
print("La moyenne est:",avg)
La moyenne est: 34.0

Binarisons les données avec ce nouveau seuil grâce à notre fonction binarise :

In [34]:
seuil = 34
train_data_bin = pd.DataFrame(binarise(np.array(train_data),seuil))
test_data_bin = pd.DataFrame(binarise(np.array(test_data),seuil))
val_data_bin = pd.DataFrame(binarise(np.array(val_data),seuil))

Entraînons ensuite le modèle avec les paramètres issus de la configuration du modèle 2 :

In [35]:
#hyper-paramètres
n_cc = 2 # Nombre de couches cachées
p_cc = {'n_neur':[450, 350], 'act':['relu','relu']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class': 10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
In [36]:
# entraînement
model_bin = Model_PMC(n_cc,p_cc,p_comp,p_sort) 
model_entrain_bin = model_bin.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=20,batch_size=128, verbose=0)
loss_train_bin = model_entrain_bin.history['loss']
acc_train_bin = model_entrain_bin.history['accuracy']
loss_val_bin = model_entrain_bin.history['val_loss']
acc_val_bin = model_entrain_bin.history['val_accuracy']
In [37]:
# Fonction de perte et accuracy pour les données test
loss_test_bin, acc_test_bin= model_bin.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin[-1],3))
print("Erreur de test:",round(loss_test_bin,3))
print("Accuracy de test:",round(acc_test_bin,3))

# Representation
plot_qualit(loss_train_bin,acc_train_bin,loss_val=loss_val_bin,acc_val=acc_val_bin)
313/313 [==============================] - 2s 6ms/step - loss: 0.1030 - accuracy: 0.9726
Erreur d'appentissage: 0.034
Accuracy d'appentissage: 0.989
Erreur de test: 0.103
Accuracy de test: 0.973

Finalement, on constate que cette configuration est moins bien adaptée sur les données binarisées. En effet, on obtient une précision de 0.9726 sur l'ensemble test contre 0.9821 pour les données normalisées ce qui correspond à une baisse de 0,9 % en terme d'accuracy.

In [ ]:
#sauvegarde
model_bin.save("model_bin.h5") 

Modèle 4 : Ajout d'une troisième couche pour améliorer le modèle n°3¶

Au vu des résultats obtenus avec le modèle n°3 (où seuil = 34), nous allons expérimenter la même configuration mais en ajoutant une troisième couche cachée à 250 neurones, doubler le nombre d'epochs et le batch_size, modifier les fonctions d'activation des couches cachées pour tenter d'obtenir une meilleure accuracy :

In [40]:
# hyper-paramètres
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[450,350,250],'act':['sigmoid','sigmoid','sigmoid']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
In [41]:
# Entraînement
model_bin1 = Model_PMC(n_cc,p_cc,p_comp,p_sort) 
model_entrain_bin1 = model_bin1.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=40,batch_size=256,verbose=0)
loss_train_bin1 = model_entrain_bin1.history['loss']
acc_train_bin1 = model_entrain_bin1.history['accuracy']
loss_val_bin1 = model_entrain_bin1.history['val_loss']
acc_val_bin1 = model_entrain_bin1.history['val_accuracy']
In [42]:
# Fonction de perte et accuracy pour les données test
loss_test_bin1, acc_test_bin1= model_bin1.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin1[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin1[-1],3))
print("Erreur de test:",round(loss_test_bin1,3))
print("Accuracy de test:",round(acc_test_bin1,3))

# Representation
plot_qualit(loss_train_bin1,acc_train_bin1,loss_val=loss_val_bin1,acc_val=acc_val_bin1)
313/313 [==============================] - 2s 6ms/step - loss: 0.0949 - accuracy: 0.9750
Erreur d'appentissage: 0.013
Accuracy d'appentissage: 0.996
Erreur de test: 0.095
Accuracy de test: 0.975
In [44]:
#Sauvegarde
model_bin1.save=("model_bin1.h5")

L'accuracy augmente de 0.02%. Nous allons tenter ainsi de modifier les nombres de neurones dans chacune des couches cachées toujours pour améliorer notre modèle.

Modèle 5 : Amélioration du modèle 4 ( Modification du nombre de neurones)¶

On passe donc de (450,350,250) à (784,256,128) où 784 représente le nombre de variable de notre jeu de données et 256 le nombre de nuances de gris, ce qui correspond à rajouter 128 neurones à notre modèle avec une première couche plus importante.

In [45]:
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[784,256,128],'act':['sigmoid','sigmoid','sigmoid']} #nombre de neurones des couches cachées et fonction d'activµation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
In [46]:
# Application 
model_bin2 = Model_PMC (n_cc,p_cc,p_comp,p_sort) 
model_entrain_bin2 = model_bin2 .fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=40,batch_size=256,verbose=0)
loss_train_bin2 = model_entrain_bin2.history['loss']
acc_train_bin2 = model_entrain_bin2.history['accuracy']
loss_val_bin2 = model_entrain_bin2.history['val_loss']
acc_val_bin2 = model_entrain_bin2.history['val_accuracy']
In [47]:
# Fonction de perte et accuracy pour les données test
loss_test_bin2, acc_test_bin2= model_bin2.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin2[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin2[-1],3))
print("Erreur de test:",round(loss_test_bin2,3))
print("Accuracy de test:",round(acc_test_bin2,3))

# Representation
plot_qualit(loss_train_bin2,acc_train_bin2,loss_val=loss_val_bin2,acc_val=acc_val_bin2)
313/313 [==============================] - 1s 3ms/step - loss: 0.0889 - accuracy: 0.9771
Erreur d'appentissage: 0.006
Accuracy d'appentissage: 0.999
Erreur de test: 0.089
Accuracy de test: 0.977
In [53]:
# Sauvegarde
model_bin2.save=("model_bin_2.h5")

L'accuracy sur l'ensemble test augmente à nouveau de 0.2% et l'accuracy des données test est à environ *0.999*. Augmentons ainsi le nombre d'epochs en le fixant à 50 au lieu de 40 afin d'observer si le modèle apprend mieux :

Modèle 6 : Modification du modèle 5 en augmentant le nombre d'epochs¶

In [48]:
#hyper-paramètres
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[784,256,128],'act':['sigmoid','sigmoid','sigmoid']} #nombre de neuronnes des couches cachées et fonction d'activµation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
In [52]:
# Application 
model_bin3 = Model_PMC (n_cc,p_cc,p_comp,p_sort) 
model_entrain_bin3 = model_bin3.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=50,batch_size=256,verbose=0)
loss_train_bin3 = model_entrain_bin3.history['loss']
acc_train_bin3 = model_entrain_bin3.history['accuracy']
loss_val_bin3 = model_entrain_bin3.history['val_loss']
acc_val_bin3 = model_entrain_bin3.history['val_accuracy']
In [54]:
# Fonction de perte et accuracy pour les données test
loss_test_bin3, acc_test_bin3= model_bin3.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin3[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin3[-1],3))
print("Erreur de test:",round(loss_test_bin3,3))
print("Accuracy de test:",round(acc_test_bin3,3))

# Representation
plot_qualit(loss_train_bin3,acc_train_bin3,loss_val=loss_val_bin3,acc_val=acc_val_bin3)
313/313 [==============================] - 1s 3ms/step - loss: 0.0751 - accuracy: 0.9810
Erreur d'appentissage: 0.008
Accuracy d'appentissage: 0.997
Erreur de test: 0.075
Accuracy de test: 0.981
In [55]:
#Sauvegarde
model_bin3.save=("model_bin_3.h5")

Nous avons finalement réussi à obtenir une accuracy de test à 0.981 (+0.04) qui est assez proche du modèle 2 trouvé pour les données normalisées.

Visualisation des mauvaises prédictions avec le modèle 6¶
In [56]:
# Prédictions test
predict_proba_bin = model_bin3.predict(test_data_bin);
predictions_bin= np.argmax(predict_proba_bin, axis=1);
print("Représentation de quelques mauvaises prédictions:")
plot_img(test_data_bin[y_test!=predictions_bin][0:5],label=predictions_bin[y_test!=predictions_bin][0:5],prediction=True)
313/313 [==============================] - 1s 3ms/step
Représentation de quelques mauvaises prédictions:
Matrice de confusion¶
In [57]:
mc_bin = pd.DataFrame(confusion_matrix(y_test,predictions_bin),columns=range(0,10))
mc_bin['Total mauvaises predict'] = [np.sum(mc_bin.iloc[i])-mc_bin.loc[i,i] for i in range(0,10)]
mc_bin
Out[57]:
0 1 2 3 4 5 6 7 8 9 Total mauvaises predict
0 969 0 1 0 1 1 3 1 3 1 11
1 0 1123 3 2 0 0 3 0 4 0 12
2 5 0 1011 2 2 0 1 7 4 0 21
3 0 0 2 989 0 7 0 5 5 2 21
4 0 1 1 0 966 0 3 1 0 10 16
5 3 0 0 6 1 873 5 1 1 2 19
6 1 2 0 0 6 3 943 0 3 0 15
7 0 3 7 1 1 0 0 1005 3 8 23
8 5 1 2 2 2 1 3 3 951 4 23
9 2 4 0 4 11 1 0 6 1 980 29

Avec ce modèle, la classe 9 est très mal prédite. Plus de 11 images de sa classe sont prédites comme des 4. C'est le contraire du modèle 2 avec les données normalisées qui prédit les 4 en 9.
Afin d'essayer d'interpréter ce résultat, affichons quelques images binarisées mal prédites :

In [59]:
plot_img(test_data_bin[(y_test==9) &(predictions_bin==4)][0:11],label=predictions_bin[(y_test==9) &(predictions_bin==4)][0:11],prediction=True)
In [61]:
plot_img(test_data_bin[(y_test==4)][0:5],label=predictions_bin[(y_test==4)][0:5],prediction=False)

La mauvaise écriture manuscrite et le fait de binariser nos données nous fait perdre de la précision à notre modèle.

Conclusion ¶

Dans cette partie, nous avons créé un tableau récapitulatif de nos modèles :

  • N : Normalisé
  • B : Binarisé
In [63]:
n_couches = [2,2,2,3,3,3]
cc1_neur = [450,450,450,450,784,784]
cc2_neur = [350,350,350,350,256,256]
cc3_neur = cc3_a = [np.nan,np.nan,np.nan,250,128,128]
cc12_a =['relu','relu','relu','sigmoid','sigmoid','sigmoid']
cc3_a = [np.nan,np.nan,np.nan,'sigmoid','sigmoid','sigmoid']
epochs= [10,20,20,40,40,50]
batchs= [128,128,128,256,256,256]
acc_test = [acc_test_norm,acc_test_norm1,acc_test_bin,acc_test_bin1,acc_test_bin2,acc_test_bin3]
recap = pd.DataFrame({'Neuronnes couche 1':cc1_neur,'Neuronnes couche 2':cc2_neur,
                      'Neuronnes couche 3':cc3_neur,'Activation couche 1':cc12_a,
                      'Activation couche 2':cc12_a,'Activation couche 3':cc3_a,
                      'Epochs':epochs,'Batch':batchs,
                      'Accuracy test':acc_test,
                     'Traitement données': ['N','N','B','B','B','B']}, 
                     index=['Modèle 1','Modèle 2','Modèle 3','Modèle 4','Modèle 5','Modèle 6'])
print("Tableau recapitulatif")
recap
Tableau recapitulatif
Out[63]:
Neuronnes couche 1 Neuronnes couche 2 Neuronnes couche 3 Activation couche 1 Activation couche 2 Activation couche 3 Epochs Batch Accuracy test Traitement données
Modèle 1 450 350 NaN relu relu NaN 10 128 0.9827 N
Modèle 2 450 350 NaN relu relu NaN 20 128 0.9821 N
Modèle 3 450 350 NaN relu relu NaN 20 128 0.9726 B
Modèle 4 450 350 250.0 sigmoid sigmoid sigmoid 40 256 0.9750 B
Modèle 5 784 256 128.0 sigmoid sigmoid sigmoid 40 256 0.9771 B
Modèle 6 784 256 128.0 sigmoid sigmoid sigmoid 50 256 0.9810 B

Meilleur modèle en terme d'accuracy de test : *Modèle 1*.

Il existe une infinité de configurations permettant de classifier les images de la base MNIST et il n'existe pas de solution "miracle". Afin de trouver le meilleur modèle MLP, nous sommes obligés de passer par une expérimentation en testant diverses combinaisons de paramètres car on ne sait pas d’avance quel est le choix optimal.

Ainsi, il est inévitable de passer par le nettoyage et la transformation des données pour éviter des problèmes d'ajustement lors de l'entraînement d'un tel modèle. Les résultats obtenus pour le modèle n°1 et le modèle n°6 confirment le fait que l'adaptation des paramètres dépend du jeu de données dont nous disposons ou du traitement que nous effectuons. En effet, notre première technique de traitement revient à normaliser les données pour les rendre homogènes alors que notre deuxième technique consiste à binariser les données, on se retrouve donc avec des valeurs égales à 0 ou à 1 et donc uniquement à des pixels noirs ou blancs. En enlevant les nuances de gris, on est suceptible de perdre de l'information notamment si nous sommes face à un jeu de données beaucoup plus complexe que celui des images MNIST ou si on choisit un seuil de binarisation trop élevé.

Par ailleurs, il faut réussir à faire un compromis entre une bonne précision et une faible perte possible tout en surveillant le problème d'overfitting ou d'underfitting grâce aux graphiques (on observe une bonne convergence de l'accuracy et de la fonction de perte pour chacun des modèles expérimentés). Comme nous l'avons vu tout au long de ce travail, la complexité d'un modèle MLP ainsi que sa performance se définissent principalement par l'initialisation des paramètres tels que le nombre de couches cachées, le nombre de neurones, le nombre d'epochs ainsi que le nombre de batchs.

Dans notre cas, c'est finalement le modèle n°1 que nous allons choisir pour prédire les données MNIST, en obtenant une accuracy test de 0.9827 (très bonne capacité de généralisation du modèle) et d'apprentissage à 0,997.

In [ ]: